﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.IO;
using ArgusLib.Threading;
using SBitmap = System.Drawing.Bitmap;
using Bitmap = ArgusLib.Drawing.ExtendedBitmap;

namespace ArgusLib.Drawing
{
	public interface IColor
	{
		int A { get; set; }
		int R { get; set; }
		int G { get; set; }
		int B { get; set; }


	}
	//public struct Color32
	//{
	//	public int Argb { get; private set; }

	//	public Color32(int argb) { this.Argb = argb; }
	//	public Color32(byte a, byte r, byte g, byte b)
	//	{
	//		this.A = a;
	//		this.R = r;
	//		this.G = g;
	//		this.B = b;
	//	}

	//	public byte A
	//	{
	//		get { return (byte)((this.Argb >> 24) & 255); }
	//		set { this.Argb &= ~(255 << 24) | (int)value << 24; }
	//	}

	//	public ushort R
	//	{
	//		get { return (byte)((this.Argb >> 16) & 255); }
	//		set { this.Argb &= ~(255 << 16) | (int)value << 16; }
	//	}

	//	public ushort G
	//	{
	//		get { return (byte)((this.Argb >> 8) & 255); }
	//		set { this.Argb &= ~(255 << 8) | (int)value << 8; }
	//	}

	//	public ushort B
	//	{
	//		get { return (byte)(this.Argb & 255); }
	//		set { this.Argb &= ~255 | (int)value; }
	//	}
	//}

	public struct Color64
	{
		public long Argb { get; private set; }

		public Color64(long argb)
			: this() { this.Argb = argb; }
		public Color64(ushort a, ushort r, ushort g, ushort b)
			:this()
		{
			this.A = a;
			this.R = r;
			this.G = g;
			this.B = b;
		}

		public ushort A
		{
			get { return (ushort)((this.Argb >> 48) & (long)ushort.MaxValue); }
			set { this.Argb &= ~((long)ushort.MaxValue << 48) | (long)value << 48; }
		}

		public ushort R
		{
			get { return (ushort)((this.Argb >> 32) & (long)ushort.MaxValue); }
			set { this.Argb &= ~((long)ushort.MaxValue << 32) | (long)value << 32; }
		}

		public ushort G
		{
			get { return (ushort)((this.Argb >> 16) & (long)ushort.MaxValue); }
			set { this.Argb &= ~((long)ushort.MaxValue << 16) | (long)value << 16; }
		}

		public ushort B
		{
			get { return (ushort)(this.Argb & (long)ushort.MaxValue); }
			set { this.Argb &= ~((long)ushort.MaxValue) | (long)value; }
		}

		public static implicit operator Color64(Color color)
		{
			return new Color64(
				(ushort)(color.A / 255.0 * ushort.MaxValue),
				(ushort)(color.R / 255.0 * ushort.MaxValue),
				(ushort)(color.G / 255.0 * ushort.MaxValue),
				(ushort)(color.B / 255.0 * ushort.MaxValue));
		}

		public static explicit operator Color(Color64 color)
		{
			return Color.FromArgb(
				(byte)(color.A * 255.0 / ushort.MaxValue),
				(byte)(color.R * 255.0 / ushort.MaxValue),
				(byte)(color.G * 255.0 / ushort.MaxValue),
				(byte)(color.B * 255.0 / ushort.MaxValue));
		}
	}

	/// <summary>
	/// ToDo: Support bitDepth = 64
	/// - Split into one base class and two classe for 32 and 64 bits per pixel
	/// </summary>
	public class ExtendedBitmap : IDisposable
	{
		BitmapData data;
		Rectangle rect;
		SBitmap bitmap;
		bool locked;
		PixelFormat pxf;
		bool doNotDispose = false;
		Action<int, int, Color> setPixel32;
		Action<int, int, Color64> setPixel64;
		Func<Color, int, int> getPixel32;
		Func<Color64, int, int> getPixel64;

		private ExtendedBitmap() { }

		public ExtendedBitmap(Image image)
			:this()
		{
			this.bitmap = new SBitmap(image);
			this.rect = new Rectangle(0, 0, image.Width, image.Height);
			this.pxf = PixelFormat.Format32bppArgb;
		}

		private ExtendedBitmap(PixelFormat pixelFormat)
		{
			this.BlendMode = Drawing.BlendMode.None;
			this.locked = false;

			this.pxf = pixelFormat;

			if (pixelFormat == PixelFormat.Format32bppArgb)
			{
				this.setPixel32 = new Action<int, int, Color>(this.SetPixel_32bppArgb);
				this.getPixel32 = new Func<Color, int, int>(this.GetPixel_32bppArgb);
			}
			else if (pixelFormat == PixelFormat.Format64bppArgb)
			{
				this.setPixel64 = new Action<int, int, Color64>(this.SetPixel_64bppArgb);
				this.getPixel64 = new Func<Color64, int, int>(this.GetPixel_64bppArgb);
			}
			else
			{
				throw new ArgumentException("Unsupported PixelFormat.");
			}
		}

		public ExtendedBitmap(int width, int height)
			: this()
		{
			this.bitmap = new SBitmap(width, height);
			this.rect = new Rectangle(0, 0, width, height);
			this.pxf = PixelFormat.Format32bppArgb;
		}

		public static Bitmap FromBitmap(SBitmap bitmap)
		{
			Bitmap RetVal = new Bitmap();
			RetVal.bitmap = bitmap;
			RetVal.rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
			RetVal.pxf = PixelFormat.Format32bppArgb;
			RetVal.doNotDispose = true;
			return RetVal;
		}

		public ExtendedBitmap(Image SourceImage, Rectangle SourceRectangle, GraphicsUnit unit)
			: this()
		{
			this.bitmap = new SBitmap(SourceRectangle.Width, SourceRectangle.Height);
			this.rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
			using (Graphics g = Graphics.FromImage(this.bitmap))
			{
				g.DrawImage(SourceImage, this.rect, SourceRectangle, unit);
			}
			this.pxf = PixelFormat.Format32bppArgb;
		}

		public bool IsLocked { get { return this.locked; } }
		public BlendMode BlendMode { get; set; }

		#region Set/Get Pixel
		long GetOffset(int x, int y, int bpp)
		{
			return this.data.Scan0.ToInt64() + (y * this.data.Stride + x * bpp / 8);
		}

		void SetPixel_32bppArgb(int x, int y, Color c)
		{
			IntPtr offset = new IntPtr(this.GetOffset(x, y, 32));
			Marshal.WriteInt32(offset, c.ToArgb());
		}

		Color GetPixel_32bppArgb(int x, int y)
		{
			IntPtr offset = new IntPtr(this.GetOffset(x, y, 32));
			int argb = Marshal.ReadInt32(offset);
			return Color.FromArgb(argb);
		}

		void SetPixel_24bppRgb(int x, int y, Color c)
		{
			long offset = this.GetOffset(x, y, 24);
			Marshal.WriteByte(new IntPtr(offset), c.R);
			Marshal.WriteByte(new IntPtr(offset + 1), c.G);
			Marshal.WriteByte(new IntPtr(offset + 2), c.B);
		}

		Color GetPixel_24bppRgb(int x, int y)
		{
			long offset = this.GetOffset(x, y, 24);
			byte r = Marshal.ReadByte(new IntPtr(offset));
			byte g = Marshal.ReadByte(new IntPtr(offset + 1));
			byte b = Marshal.ReadByte(new IntPtr(offset + 2));
			return Color.FromArgb(r, g, b);
		}

		void SetPixel_64bppArgb(int x, int y, Color64 c)
		{
			IntPtr offset = new IntPtr(this.data.Scan0.ToInt64() + (y * this.data.Stride + x * 8));
			Marshal.WriteInt64(offset, c.Argb);
		}

		Color64 GetPixel_64bppArgb(int x, int y)
		{
			IntPtr offset = new IntPtr(this.data.Scan0.ToInt64() + (y * this.data.Stride + x * 8));
			return new Color64(Marshal.ReadInt64(offset));
		}

		void SetPixel_48bppRgb(int x, int y, Color64 c)
		{
			long offset = this.GetOffset(x, y, 48);
			Marshal.WriteInt16(new IntPtr(offset), (short)c.R);
			Marshal.WriteInt16(new IntPtr(offset + 2), (short)c.G);
			Marshal.WriteInt16(new IntPtr(offset + 4), (short)c.B);
		}

		Color64 GetPixel_48bppRgb(int x, int y)
		{
			long offset = this.GetOffset(x, y, 48);
			ushort r = (ushort)Marshal.ReadInt16(new IntPtr(offset));
			ushort g = (ushort)Marshal.ReadInt16(new IntPtr(offset + 2));
			ushort b = (ushort)Marshal.ReadInt16(new IntPtr(offset + 3));
			return new Color64(ushort.MaxValue, r, g, b);
		}

		public void SetPixel(int x, int y, Color c)
		{
			if (this.locked == false)
			{
				//this.bitmap.SetPixel(x, y, c);
				//return;
				throw new InvalidOperationException("The bitmap must be locked.");
			}

			if (this.setPixel32 == null)
			{
				throw new InvalidOperationException("The pixelformat doesn't support 32 bits per pixel");
			}

			this.setPixel32(x, y, c);
		}

		public Color GetPixel(int x, int y)
		{
			if (this.locked == false)
			{
				//return this.bitmap.GetPixel(x, y);
				throw new InvalidOperationException("The bitmap must be locked.");
			}

			if (this.getPixel32 == null)
			{
				throw new InvalidOperationException("The pixelformat doesn't support 32 bits per pixel");
			}

			return this.getPixel32(x, y);
		}

		public void SetPixel64(int x, int y, Color64 c)
		{
			if (this.locked == false)
			{
				//this.bitmap.SetPixel(x, y, c);
				//return;
				throw new InvalidOperationException("The bitmap must be locked.");
			}

			if (this.setPixel64 == null)
			{
				throw new InvalidOperationException("The pixelformat doesn't support 64 bits per pixel");
			}

			this.setPixel64(x, y, c);
		}

		public Color64 GetPixel64(int x, int y)
		{
			if (this.locked == false)
			{
				//return this.bitmap.GetPixel(x, y);
				throw new InvalidOperationException("The bitmap must be locked.");
			}

			if (this.getPixel64 == null)
			{
				throw new InvalidOperationException("The pixelformat doesn't support 64 bits per pixel");
			}

			return this.getPixel64(x, y);
		}
		#endregion

		public void Clear(Color color)
		{
			bool isLocked = this.IsLocked;
			if (isLocked == false)
				this.Lock();

			for (int x = 0; x < this.Width; x++)
			{
				for (int y = 0; y < this.Height; y++)
				{
					this.SetPixel(x, y, color);
				}
			}

			if (isLocked == false)
				this.Unlock();
		}

		public void Fill(Color color)
		{
			bool isLocked = this.IsLocked;
			if (isLocked == false)
				this.Lock();

			for (int x = 0; x < this.Width; x++)
			{
				for (int y = 0; y < this.Height; y++)
				{
					this.BlendPixel(x, y, color);
				}
			}

			if (isLocked == false)
				this.Unlock();
		}

		public bool Lock()
		{
			if (this.locked == true)
				return false;

			this.data = this.bitmap.LockBits(this.rect, ImageLockMode.ReadWrite, this.pxf);
			this.locked = true;
			return true;
		}

		public void BlendPixel(int x, int y, Color source)
		{
			this[x, y] = ColorExtension.Blend(source, this[x, y], this.BlendMode);
		}

		public Color this[int x, int y]
		{
			get { return this.GetPixel(x, y); }
			set { this.SetPixel(x, y, value); }
		}

		public bool Unlock()
		{
			if (this.locked == false)
				return false;

			this.bitmap.UnlockBits(this.data);
			this.locked = false;
			return true;
		}
		
		/// <summary>
		/// Inverts the color in the whole image.
		/// </summary>
		public void InvertColor()
		{
			for (int x = 0; x < this.Width; x++)
			{
				for (int y = 0; y < this.Height; y++)
				{
					this.InvertColor(x, y);
				}
			}
		}

		/// <summary>
		/// Inverts the color of a pixel.
		/// </summary>
		/// <param name="x">x coordinate of the pixel.</param>
		/// <param name="y">y coordinate of the pixel.</param>
		public void InvertColor(int x, int y)
		{
			this.SetPixel(x, y, Bitmap.InvertColor(this.GetPixel(x, y)));
		}

		/// <summary>
		/// Inverts the color of an array of pixels.
		/// </summary>
		/// <param name="Points">Point-array containing the coordinates of the pixels.</param>
		public void InvertColor(Point[] Points)
		{
			foreach (Point p in Points)
			{
				this.InvertColor(p.X, p.Y);
			}
		}

		/// <summary>
		/// Inverts pixels using a mask. Each pixel which is colored black in the mask
		/// is inverted.
		/// </summary>
		/// <param name="mask">The mask to be used to invert the colors.</param>
		/// <param name="TopLeft">The position where the mask will be applied.</param>
		public void InvertColor(Image mask, Point TopLeft)
		{
			using (Bitmap t = new Bitmap(mask))
			{
				t.Lock();
				for (int x = 0; x < mask.Width; x++)
				{
					for (int y = 0; y < mask.Height; y++)
					{
						if (t.GetPixel(x, y) != Color.Black)
							continue;

						Point p = new Point(TopLeft.X + x, TopLeft.Y + y);
						if (this.Size.Contains(p) == false)
							continue;

						this.InvertColor(x, y);
					}
				}
				t.Unlock();
			}
		}

		/// <summary>
		/// Inverts the given color.
		/// </summary>
		/// <param name="c">The color to invert.</param>
		/// <returns>The inverted color.</returns>
		public static Color InvertColor(Color c)
		{
			return Color.FromArgb(c.A, 255 - c.R, 255 - c.G, 255 - c.B);
		}

		public SBitmap GetBitmap()
		{
			if (this.locked == true)
				throw new Exception("The GetBitmap-Method may only be called when the Bitmap is unlocked.");
			return new SBitmap(this.bitmap);
		}

		public Bitmap GetSection(Rectangle bounds)
		{
			Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
			bitmap.Lock();

			for (int x = 0; x < bounds.Width; x++)
			{
				for (int y = 0; y < bounds.Height; y++)
				{
					bitmap.SetPixel(x, y, this.GetPixel(x + bounds.X, y + bounds.Y));
				}
			}

			bitmap.Unlock();
			return bitmap;
		}

		public Bitmap RemoveTransparency(Color Background)
		{
			SBitmap Ret = new SBitmap(this.bitmap.Width, this.bitmap.Height);
			Graphics g = Graphics.FromImage(Ret);
			g.Clear(Background);
			g.DrawImage(this.bitmap, new Rectangle(new Point(0, 0), this.bitmap.Size));
			g.Dispose();
			return new Bitmap(Ret);
		}

		public Bitmap MakeTransparent(Color Background)
		{
			Bitmap b = this.RemoveTransparency(Background);
			b.Lock();

			for (int x = 0; x < b.Width; x++)
			{
				for (int y = 0; y < b.Height; y++)
				{
					Color c = b.GetPixel(x, y);
					double a = this.MakeTransparent_GetAlpha(c, Background);
					b.SetPixel(x, y, this.MakeTransparent_GetColor(a, c, Background));
				}
			}

			b.Unlock();
			return b;
		}

		#region MakeTransparentHelpMethods
		private double MakeTransparent_GetAlpha(Color c, Color BackColor)
		{
			double a = this.MakeTransparent_GetAlphaC(c.R, BackColor.R);
			a = System.Math.Max(a, this.MakeTransparent_GetAlphaC(c.G, BackColor.G));
			a = System.Math.Max(a, this.MakeTransparent_GetAlphaC(c.B, BackColor.B));
			return a;
		}

		private double MakeTransparent_GetAlphaC(byte c, byte c_b)
		{
			if (c == c_b)
			{
				return 0;
			}
			if (c > c_b)
			{
				return (c - c_b) / (double)(255 - c_b);
			}
			else
			{
				return (c_b - c) / (double)c_b;
			}
		}

		private byte MakeTransparent_GetColorC(double a, byte c, byte c_b)
		{
			if (a == 0)
				return 0;
			return (byte)((c - c_b) / a + c_b);
		}

		private Color MakeTransparent_GetColor(double a, Color c, Color BackColor)
		{
			return Color.FromArgb((byte)(a * 255),
				this.MakeTransparent_GetColorC(a, c.R, BackColor.R),
				this.MakeTransparent_GetColorC(a, c.G, BackColor.G),
				this.MakeTransparent_GetColorC(a, c.B, BackColor.B));
		}
		#endregion

		public int Width { get { return this.rect.Width; } }
		public int Height { get { return this.rect.Height; } }
		public Size Size { get { return this.rect.Size; } }

		public void Dispose()
		{
			if (this.IsLocked == true)
				this.Unlock();

			if (this.doNotDispose == false)
				this.bitmap.Dispose();
		}

		[RequiresAssembly(typeof(Parallelization))]
		public void SplitAndSave(string[,] filenames, string destFolder, ImageFormat imgFormat, string fileExt, Parallelization.ProgressChangedHandler progressChanged)
		{
			if (Directory.Exists(destFolder) == false)
				throw new IOException("Folder does not exist:\n" + destFolder);

			int length0 = filenames.GetLength(0);
			int length1 = filenames.GetLength(1);
			int sectionWidth = this.Width / length0;
			int sectionHeight = this.Height / length1;

			Parallelization.For(length0, new Parallelization.ForBody((x, threadState) =>
				{
					for (int y = 0; y < length1; y++)
					{
						if (string.IsNullOrEmpty(filenames[x, y]) == true)
							continue;
						Bitmap b = this.GetSection(new Rectangle(x * sectionWidth, y * sectionHeight, sectionWidth, sectionHeight));
						string path = Path.Combine(destFolder, filenames[x, y]+fileExt);
						b.GetBitmap().Save(path, imgFormat);
					}
				}), new ThreadState(), progressChanged);
		}

		[RequiresAssembly(typeof(Parallelization))]
		public void SplitAndSave(string[,] filenames, string destFolder, ImageFormat imgFormat)
		{
			this.SplitAndSave(filenames, destFolder, imgFormat, string.Empty, null);
		}

		public bool Multiply(Bitmap factor)
		{
			if (this.Size != factor.Size)
				return false;

			bool isLocked = this.IsLocked;
			if (isLocked == false)
				this.Lock();

			for (int x = 0; x < this.Width; x++)
			{
				for (int y = 0; y < this.Height; y++)
				{
					Color c1 = this.GetPixel(x, y);
					Color c2 = factor.GetPixel(x, y);
					Color c = Color.FromArgb(
						(byte)(c1.A * c2.A / 255.0),
						(byte)(c1.R * c2.R / 255.0),
						(byte)(c1.G * c2.G / 255.0),
						(byte)(c1.B * c2.B / 255.0));
					this.SetPixel(x, y, c);
				}
			}

			if (isLocked == false)
				this.Unlock();
			return true;
		}

		//public static explicit operator SBitmap(Bitmap bitmap)
		//{
		//	return bitmap.bitmap;
		//}
	}
}
